Financial Market Instruments - Unique Identifiers

This tutorial was originally published on DataCareer.

In financial markets, tradable instruments and securities have unique identifiers. The identifiers are very useful, because you can make sure that you and your counterparty are talking about the same instrument while trading. The difficulty is that there isn't really a standard for all the various sorts of instruments or markets. Anyone working in the industry will recognize this issue, especially people working at larger institutions who deal with a variety of instruments. Products like equities, bonds (fixed income), indices, derivatives, currencies and structured products all have their own conventions. Just to name a few identifiers, you may come across:

  • Ticker
  • ISIN: International Securities Identification Number
  • CUSIP: Committee on Uniform Securities Identification Procedures (U.S.A. & Canada)
  • SEDOL: Stock Exchange Daily Official List (United Kingdom)
  • VALOR: (Switzerland)
  • WKN: Wertpapierkennnummer (Germany)
  • APIR: Asia Pacific Investment Register (Australia)
  • FIGI: Financial Instrument Global Identifier
  • BBGID: Bloomberg Global Identfier
  • RIC: Reuters Instrument Code
  • MIC: Market Identifier Code
  • LEI: Legal Entitiy Identifier

Fortunately, the problem is recognized and there are steps being made to tackle this. Bloomberg has initiated OpenFigi and Refinitiv (formerly Thomson Reuters) PermID. Basically, they are open sourcing their proprietary identifiers. This makes it easier to map instruments to other identifiers which you may use.

Please take a look at the following websites, before continuing this tutorial:

  1. OpenFIGI: https://openfigi.com
  2. PermID: https://permid.org

For OpenFIGI we will use some of the example code which is published at: https://github.com/OpenFIGI/api-examples/blob/master/python/example-with-requests.py. @Jaret, thanks for sharing!

As always, let's start with importing some packages:

In [1]:
import requests     # 2.19.1
import json         # 2.0.9
import pandas as pd # 0.23.4
import pprint

We will get the identifiers for a handful of companies listed in the DAX (Deutscher Aktienindex) in Germany. It is a blue chip stock market index consisting of the 30 major German companies trading on the Frankfurt Stock Exchange. We randomly chose 5 tickers, feel free to change the list or add any you may like.

In [2]:
tickers = ['ADS', 'BAS', 'DTE', 'SAP', 'SIE'] # Adidas, BASF, Deutsche Telekom, SAP & Siemens 

The Market Identifier Code (MIC) for Xetra in Frankfurt Germany is XETR. If we specify this in our mapping requests, we have a better chance of getting the right identifiers back. Because some tickers my refer to other companies somewhere else in the world. For clarity, we write every mapping job out fully. For bigger lists, it is probably better to use a loop.

In [3]:
jobs = [
    {'idType': 'TICKER', 'idValue': 'ADS', 'micCode': 'XETR'},
    {'idType': 'TICKER', 'idValue': 'BAS', 'micCode': 'XETR'},    
    {'idType': 'TICKER', 'idValue': 'DTE', 'micCode': 'XETR'},
    {'idType': 'TICKER', 'idValue': 'SAP', 'micCode': 'XETR'},
    {'idType': 'TICKER', 'idValue': 'SIE', 'micCode': 'XETR'}
]
In [4]:
# Alternatively, if you start with ISIN codes:
# jobs = [{'idType': 'ID_ISIN', 'idValue': 'DE0005557508', 'micCode': 'XETR'}]

Now we have defined the jobs, we need to set some of the configuration and define the function we can use for the jobs. You don't necessarily need an API key for FIGI, but with one you will have higher rate limits.

In [5]:
openfigi_apikey = ''  # Please put your own API Key here
openfigi_url = 'https://api.openfigi.com/v2/mapping'
openfigi_headers = {'Content-Type': 'text/json'}
In [6]:
def map_jobs(jobs):
    if openfigi_apikey:
        openfigi_headers['X-OPENFIGI-APIKEY'] = openfigi_apikey
    response = requests.post(url=openfigi_url, headers=openfigi_headers,
                             json=jobs)
    if response.status_code != 200:
        raise Exception('Bad response code {}'.format(str(response.status_code)))
    return response.json()

Now let's execute the function and print the results to the screen.

In [7]:
job_results = map_jobs(jobs)

pprint.pprint(job_results)
[{'data': [{'compositeFIGI': 'BBG000FR1Q22',
            'exchCode': 'GY',
            'figi': 'BBG000FR1RP5',
            'marketSector': 'Equity',
            'name': 'ADIDAS AG',
            'securityDescription': 'ADS',
            'securityType': 'Common Stock',
            'securityType2': 'Common Stock',
            'shareClassFIGI': 'BBG001S8J8Q3',
            'ticker': 'ADS',
            'uniqueID': 'EQ0018000100001000',
            'uniqueIDFutOpt': None}]},
 {'data': [{'compositeFIGI': 'BBG000BBVT77',
            'exchCode': 'GY',
            'figi': 'BBG000BBVVK7',
            'marketSector': 'Equity',
            'name': 'BASF SE',
            'securityDescription': 'BAS',
            'securityType': 'Common Stock',
            'securityType2': 'Common Stock',
            'shareClassFIGI': 'BBG001S5SYB8',
            'ticker': 'BAS',
            'uniqueID': 'EQ0011569400001000',
            'uniqueIDFutOpt': None}]},
 {'data': [{'compositeFIGI': 'BBG000HJTKL0',
            'exchCode': 'GY',
            'figi': 'BBG000HJTMS9',
            'marketSector': 'Equity',
            'name': 'DEUTSCHE TELEKOM AG-REG',
            'securityDescription': 'DTE',
            'securityType': 'Common Stock',
            'securityType2': 'Common Stock',
            'shareClassFIGI': 'BBG001S5T4S5',
            'ticker': 'DTE',
            'uniqueID': 'EQ0018055600001000',
            'uniqueIDFutOpt': None}]},
 {'data': [{'compositeFIGI': 'BBG000BG7DY8',
            'exchCode': 'GY',
            'figi': 'BBG000BG7GX2',
            'marketSector': 'Equity',
            'name': 'SAP SE',
            'securityDescription': 'SAP',
            'securityType': 'Common Stock',
            'securityType2': 'Common Stock',
            'shareClassFIGI': 'BBG001S6RK27',
            'ticker': 'SAP',
            'uniqueID': 'EQ0011841200001000',
            'uniqueIDFutOpt': None}]},
 {'data': [{'compositeFIGI': 'BBG000BCCRV3',
            'exchCode': 'GY',
            'figi': 'BBG000BCCT76',
            'marketSector': 'Equity',
            'name': 'SIEMENS AG-REG',
            'securityDescription': 'SIE',
            'securityType': 'Common Stock',
            'securityType2': 'Common Stock',
            'shareClassFIGI': 'BBG001S69GV4',
            'ticker': 'SIE',
            'uniqueID': 'EQ0011574600001000',
            'uniqueIDFutOpt': None}]}]

As you can see, the result of the mapping job is a list of dictionaries. For every ticker, the 'key' is called "data". Let's change the structure a little bit, so we can easily convert it to a Pandas DataFrame. We don't need the keys named 'data', just the list of their values.

In [8]:
just_dictionaries = [d['data'][0] for d in job_results]

With this new list of dictionaries, we can directly convert it:

In [9]:
df_figi = pd.DataFrame.from_dict(just_dictionaries)
df_figi
Out[9]:
compositeFIGI exchCode figi marketSector name securityDescription securityType securityType2 shareClassFIGI ticker uniqueID uniqueIDFutOpt
0 BBG000FR1Q22 GY BBG000FR1RP5 Equity ADIDAS AG ADS Common Stock Common Stock BBG001S8J8Q3 ADS EQ0018000100001000 None
1 BBG000BBVT77 GY BBG000BBVVK7 Equity BASF SE BAS Common Stock Common Stock BBG001S5SYB8 BAS EQ0011569400001000 None
2 BBG000HJTKL0 GY BBG000HJTMS9 Equity DEUTSCHE TELEKOM AG-REG DTE Common Stock Common Stock BBG001S5T4S5 DTE EQ0018055600001000 None
3 BBG000BG7DY8 GY BBG000BG7GX2 Equity SAP SE SAP Common Stock Common Stock BBG001S6RK27 SAP EQ0011841200001000 None
4 BBG000BCCRV3 GY BBG000BCCT76 Equity SIEMENS AG-REG SIE Common Stock Common Stock BBG001S69GV4 SIE EQ0011574600001000 None

The DataFrame actually contains a little bit more information than we need for this tutorial. We can filter the interesting columns and set the tickers to be the index.

In [10]:
# Columns of interest
columns = ['ticker', 'name', 'marketSector', 'figi', 'uniqueID']
In [11]:
# Filter out the columns of interest
df_figi = df_figi[columns]
# Set the tickers to be the index
df_figi = df_figi.set_index('ticker')
# Show the DataFrame
df_figi
Out[11]:
name marketSector figi uniqueID
ticker
ADS ADIDAS AG Equity BBG000FR1RP5 EQ0018000100001000
BAS BASF SE Equity BBG000BBVVK7 EQ0011569400001000
DTE DEUTSCHE TELEKOM AG-REG Equity BBG000HJTMS9 EQ0018055600001000
SAP SAP SE Equity BBG000BG7GX2 EQ0011841200001000
SIE SIEMENS AG-REG Equity BBG000BCCT76 EQ0011574600001000

Excellent. We now have mapped tickers to their respective FIGIs. Time for the next step: PermIDs.

PermID

PermID is short for "permanent identifier". It is the open sourced identifier system of Refinitiv (formerly Thomson Reuters). You do need to register at the Developer Portal to be able to use their APIs. In this tutorial, we will use the Record Matching API to map tickers to their respective PermID's. You can find a 'quick start' at: https://developers.refinitiv.com/en/api-catalog/open-perm-id/permid-record-matching-restful-api/quick-start.

NB: At the time of writing, it is not possible to use ISINs as input nor to get RICs as output. Hopefully this will be added to the API's functionality in the future.

Please fill in you own access token before continuing:

In [22]:
# Your own access token
access_token = ''

Plus we define the configuration:

In [13]:
# API endpoint
request_url = "https://api.thomsonreuters.com/permid/match"

headers = {
    'Content-Type': 'text/plain',
    'Accept': 'application/json',
    'x-ag-access-token': access_token,
    'x-openmatch-numberOfMatchesPerRecord': '1',   # only return 1 match per ticker
    'x-openmatch-dataType': 'Organization',        # only match to "organizations", not "persons" or other tags
}

Now it gets a little bit more tricky. As you can read in the documentation, the API excepts a CSV file or "specially formatted text" as input. We are working with a list of tickers in this tutorial, so we need to construct the "specially formatted text" ourselves.

In [14]:
# The first line in the text field is 'Standard Identifier'. We use 'Ticker' as identifier in this tutorial
text_field = 'Standard Identifier\n'

# For every ticker, we will add a new line and specify the Market Identifier Code (MIC) / Exchange
exchange = 'XETR'
for ticker in tickers:
    identifier = 'TICKER:' + ticker + '&&MIC:' + exchange + '\n'
    text_field += identifier

# Print the 'text_field' to screen for inspection
print(text_field)
Standard Identifier
TICKER:ADS&&MIC:XETR
TICKER:BAS&&MIC:XETR
TICKER:DTE&&MIC:XETR
TICKER:SAP&&MIC:XETR
TICKER:SIE&&MIC:XETR

Now it is time to construct & make the actual request.

In [15]:
response = requests.post(request_url, headers=headers, data=text_field)
r = response.json()

The information are looking for is in 'outputContentResponse'. We can print it to the screen for inspection.

In [16]:
# Pretty print the response to the screen
pprint.pprint(r['outputContentResponse'])
[{'Input_Standard Identifier': 'TICKER:ADS&&MIC:XETR',
  'Match Level': 'Excellent',
  'Match OpenPermID': 'https://permid.org/1-4295868725',
  'Match Ordinal': '1',
  'Match OrgName': 'Adidas AG',
  'Match Score': '100%',
  'Original Row Number': '2',
  'ProcessingStatus': 'OK'},
 {'Input_Standard Identifier': 'TICKER:BAS&&MIC:XETR',
  'Match Level': 'Excellent',
  'Match OpenPermID': 'https://permid.org/1-4295869198',
  'Match Ordinal': '1',
  'Match OrgName': 'BASF SE',
  'Match Score': '100%',
  'Original Row Number': '3',
  'ProcessingStatus': 'OK'},
 {'Input_Standard Identifier': 'TICKER:DTE&&MIC:XETR',
  'Match Level': 'Excellent',
  'Match OpenPermID': 'https://permid.org/1-4295870332',
  'Match Ordinal': '1',
  'Match OrgName': 'Deutsche Telekom AG',
  'Match Score': '100%',
  'Original Row Number': '4',
  'ProcessingStatus': 'OK'},
 {'Input_Standard Identifier': 'TICKER:SAP&&MIC:XETR',
  'Match Level': 'Excellent',
  'Match OpenPermID': 'https://permid.org/1-5043321284',
  'Match Ordinal': '1',
  'Match OrgName': 'SAP SE',
  'Match Score': '100%',
  'Original Row Number': '5',
  'ProcessingStatus': 'OK'},
 {'Input_Standard Identifier': 'TICKER:SIE&&MIC:XETR',
  'Match Level': 'Excellent',
  'Match OpenPermID': 'https://permid.org/1-4295869238',
  'Match Ordinal': '1',
  'Match OrgName': 'Siemens AG',
  'Match Score': '100%',
  'Original Row Number': '6',
  'ProcessingStatus': 'OK'}]

Let's quickly loop through the results to check out the PermID's.

In [17]:
for company in r['outputContentResponse']:
    print(company['Match OrgName'] + ' --> ' + company['Match OpenPermID'])
Adidas AG --> https://permid.org/1-4295868725
BASF SE --> https://permid.org/1-4295869198
Deutsche Telekom AG --> https://permid.org/1-4295870332
SAP SE --> https://permid.org/1-5043321284
Siemens AG --> https://permid.org/1-4295869238

Did you check out any of the links? PermID offers some additional data besides the identifier. Let's define a function which will help us to retrieve the additional data.

In [18]:
def permid_data(permid_url):
    permid_headers = {
        'Accept': 'text/turtle',
    }

    permid_params = {
        'format': 'json-ld',
        'access-token': access_token
    }

    # The actual request
    permid_response = requests.get(permid_url, headers=headers, params=permid_params)
    
    # Convert the response to JSON
    permid_data = json.loads(permid_response.content)
    
    return permid_data

Now we will create a dictionary of dictionaries, one for every ticker we are interested in. We do this so we can then later convert it easily to a Pandas DataFrame. So, we use a loop to go through each company in the list of the tickers to:

  1. retrieve the data about the respective company
  2. extract the fields we are interested in.
In [19]:
# Create an empty dictionary
permid_dict = {}

# Loop through all tickers and put the data in the dictionary
for ticker, i in zip(tickers, r['outputContentResponse']):
    # The PermID url for the ticker from the response earlier
    permid_url = i['Match OpenPermID']
    
    # Use the function defined above to download the data
    data = permid_data(permid_url)

    # Put the desired data in a dictionary for the ticker
    permid_dict[ticker] = {
        'company': data['vcard:organization-name'],
        'IPO'    : data['hasIPODate'],
        'address': data['mdaas:HeadquartersAddress'],
        'website': data['hasURL'],
        'phone'  : data['tr-org:hasHeadquartersPhoneNumber'],
        'LEI'    : data['tr-org:hasLEI'],
        'permid' : data['tr-common:hasPermId'],
        'permid_url' : permid_url
    }

All the data is now in a dictionary of dictionaries. Again, it is also very easy to convert this to a Pandas DataFrame.

In [20]:
df_permid = pd.DataFrame.from_dict(permid_dict, orient='index') # Orient='index' for data in rows instead of columns
df_permid
Out[20]:
company IPO address website phone LEI permid permid_url
ADS Adidas AG 1997-11-28T05:00:00Z Adi-Dassler-Strasse 1\nHERZOGENAURACH\nBAYERN\... https://www.adidas-group.com/de/ 499132840 549300JSX0Z4CW0V5023 4295868725 https://permid.org/1-4295868725
BAS BASF SE 1952-01-30T05:00:00Z Carl-Bosch-Str. 38\nLUDWIGSHAFEN AM RHEIN\nRHE... https://www.basf.com/ 49621600 529900PM64WH8AF1E917 4295869198 https://permid.org/1-4295869198
DTE Deutsche Telekom AG 2000-01-24T05:00:00Z Friedrich-Ebert-Allee 140\nBONN\nNORDRHEIN-WES... https://www.telekom.com/ 492281810 549300V9QSIG4WX4GJ96 4295870332 https://permid.org/1-4295870332
SAP SAP SE 1988-11-04T05:00:00Z Dietmar-Hopp-Allee 16\nWALLDORF\nBADEN-WUERTTE... https://www.sap.com/ 496227747474 529900D6BF99LW9R2E68 5043321284 https://permid.org/1-5043321284
SIE Siemens AG 2008-02-25T05:00:00Z Wittelsbacherplatz 2\nMUENCHEN\nBAYERN\n80333\... https://www.siemens.com/global/de/ 498963600 W38RGI023J3WT1HWRP32 4295869238 https://permid.org/1-4295869238

As a final touch, let's join both DataFrames:

In [21]:
df_final = df_figi.join(df_permid)
df_final
Out[21]:
name marketSector figi uniqueID company IPO address website phone LEI permid permid_url
ticker
ADS ADIDAS AG Equity BBG000FR1RP5 EQ0018000100001000 Adidas AG 1997-11-28T05:00:00Z Adi-Dassler-Strasse 1\nHERZOGENAURACH\nBAYERN\... https://www.adidas-group.com/de/ 499132840 549300JSX0Z4CW0V5023 4295868725 https://permid.org/1-4295868725
BAS BASF SE Equity BBG000BBVVK7 EQ0011569400001000 BASF SE 1952-01-30T05:00:00Z Carl-Bosch-Str. 38\nLUDWIGSHAFEN AM RHEIN\nRHE... https://www.basf.com/ 49621600 529900PM64WH8AF1E917 4295869198 https://permid.org/1-4295869198
DTE DEUTSCHE TELEKOM AG-REG Equity BBG000HJTMS9 EQ0018055600001000 Deutsche Telekom AG 2000-01-24T05:00:00Z Friedrich-Ebert-Allee 140\nBONN\nNORDRHEIN-WES... https://www.telekom.com/ 492281810 549300V9QSIG4WX4GJ96 4295870332 https://permid.org/1-4295870332
SAP SAP SE Equity BBG000BG7GX2 EQ0011841200001000 SAP SE 1988-11-04T05:00:00Z Dietmar-Hopp-Allee 16\nWALLDORF\nBADEN-WUERTTE... https://www.sap.com/ 496227747474 529900D6BF99LW9R2E68 5043321284 https://permid.org/1-5043321284
SIE SIEMENS AG-REG Equity BBG000BCCT76 EQ0011574600001000 Siemens AG 2008-02-25T05:00:00Z Wittelsbacherplatz 2\nMUENCHEN\nBAYERN\n80333\... https://www.siemens.com/global/de/ 498963600 W38RGI023J3WT1HWRP32 4295869238 https://permid.org/1-4295869238

We started with only the tickers for a handful of companies. Finally, we have for every ticker the full company name, FIGI, PermID, Legal Entity Identifier (LEI), initial public offering date, physical address, website and phone number.

Not bad, right?